using System;
using System.Collections.Generic;
using PropertyExpression.Common;

namespace PropertyExpression.ReactiveRelation
{
    public class CalcStorage<TCalcContext>
    {
        public CalcStorage(Action<ICalcRegistrator<TCalcContext>> initAction)
        {
            dependencyManager = new DependencyManager<TCalcContext>(this);

            initAction(new CalcRegistrator(this));
        }

        private readonly DependencyManager<TCalcContext> dependencyManager;

        internal DependencyManager<TCalcContext> DependencyManager
        {
            get { return dependencyManager; }
        }

        private readonly ReverseDependencyManager<TCalcContext> reverseDependencyManager = new ReverseDependencyManager<TCalcContext>();

        internal ReverseDependencyManager<TCalcContext> ReverseDependencyManager
        {
            get { return reverseDependencyManager; }
        }

        private class CalcRegistrator : ICalcRegistrator<TCalcContext>
        {
            private readonly CalcStorage<TCalcContext> parent;

            public CalcRegistrator(CalcStorage<TCalcContext> parent)
            {
                this.parent = parent;
            }

            public void Register<T>(Action<IBinder<TCalcContext, T>> action)
            {
                var entityType = typeof (T);
                action(new Binder<T>(
                           this, 
                           formulaProxy => parent.DependencyManager.Add(entityType, formulaProxy))
                    );
            }

            private class Binder<TEntity> : IBinder<TCalcContext, TEntity>
            {
                private readonly CalcRegistrator parent;
                private readonly Action<IFormulaProxy<TCalcContext>> addDependencyAction;

                public Binder(CalcRegistrator parent, Action<IFormulaProxy<TCalcContext>> addDependencyAction)
                {
                    this.parent = parent;
                    this.addDependencyAction = addDependencyAction;
                }

                public Action<Func<IPropertyBindContext<TCalcContext, TEntity, TProperty>, IValue<TProperty>>>
                    Bind<TProperty>(IPropertyExpressionProxy<TEntity, TProperty> propertyExpressionProxy)
                {
                    return
                        func =>
                            {
                                var formulaProxy = new FormulaProxy<TEntity, TProperty>(propertyExpressionProxy, func);
                                addDependencyAction(formulaProxy);
                                parent.parent.ReverseDependencyManager.Add(formulaProxy);
                            };
                }
            }
        }

        private class FormulaProxy<TEntity, TProperty> : IFormulaProxy<TCalcContext>
        {
            private readonly IPropertyExpressionProxy<TEntity, TProperty> propertyExpressionProxy;
            private readonly Func<IPropertyBindContext<TCalcContext, TEntity, TProperty>, IValue<TProperty>> func;

            public FormulaProxy(
                IPropertyExpressionProxy<TEntity, TProperty> propertyExpressionProxy,
                Func<IPropertyBindContext<TCalcContext, TEntity, TProperty>, IValue<TProperty>> func)
            {
                this.propertyExpressionProxy = propertyExpressionProxy;
                this.func = func;
            }

            private class EntityWrapper 
                : IEntityWrapper<TEntity>, IPropertyBindContext<TCalcContext, TEntity, TProperty>
            {
                private readonly Func<TEntity> entityFunc;
                private readonly FormulaProxy<TEntity, TProperty> parent;
                private readonly Func<IBindContext<TCalcContext>> bindContextFunc;

                public EntityWrapper(
                    Func<TEntity> entityFunc,
                    FormulaProxy<TEntity, TProperty> parent,
                    Func<IBindContext<TCalcContext>> bindContextFunc)
                {
                    this.entityFunc = entityFunc;
                    this.parent = parent;
                    this.bindContextFunc = bindContextFunc;
                }

                public TEntity Entity
                {
                    get { return entityFunc(); }
                }

                public IEnumerable<object> GetReverseEntities(
                    object sender, IPreviousValueProcessor previousValueProcessor)
                {
                    yield return sender;
                }

                public IPropertyValue<TResultProperty> GetPropertyValue<TResultProperty>(
                    IPropertyExpressionProxy<TEntity, TResultProperty> propertyExpressionProxy)
                {
                    return this.CreatePropertyValue(propertyExpressionProxy);
                }

                public IValue<TProperty> NotCalc()
                {
                    return new NotCalcValue();
                }

                private class NotCalcValue : IValue<TProperty>
                {
                    public IEnumerable<ICell> GetCells()
                    {
                        return new ICell[] { };
                    }

                    public IOptionalValue<TProperty> GetValue(
                        IPropertySettor propertySettor, 
                        IOneToManyCollectionNewItemProcessor oneToManyCollectionNewItemProcessor)
                    {
                        return OptionalValue.Nothing<TProperty>();
                    }
                }

                public IValue<TProperty> GetStoredValue()
                {
                    return new StoredValue(this);
                }

                private class StoredValue : IValue<TProperty>
                {
                    private readonly EntityWrapper parent;

                    public StoredValue(EntityWrapper parent)
                    {
                        this.parent = parent;
                    }

                    public IEnumerable<ICell> GetCells()
                    {
                        yield return new Cell(this);
                    }

                    private class Cell : ICell
                    {
                        private readonly StoredValue parent;

                        public Cell(StoredValue parent)
                        {
                            this.parent = parent;
                        }

                        public string PropertyName
                        {
                            get
                            {
                                return parent.parent.parent.propertyExpressionProxy.GetPropertyName();
                            }
                        }

                        public string EntityFullName
                        {
                            get { return typeof(TEntity).FullName; }
                        }

                        public IEnumerable<object> GetReverseEntities(
                            object sender, IPreviousValueProcessor previousValueProcessor)
                        {
                            return parent.parent.GetReverseEntities(sender, previousValueProcessor);
                        }
                    }

                    public IOptionalValue<TProperty> GetValue(
                        IPropertySettor propertySettor,
                        IOneToManyCollectionNewItemProcessor oneToManyCollectionNewItemProcessor)
                    {
                        return parent.parent.propertyExpressionProxy.Execute(parent.Entity)
                            .AsOptionalValue();
                    }
                }

                public FuncCache FuncCache
                {
                    get { return bindContextFunc().FuncCache; }
                }

                public TCalcContext CalcContext
                {
                    get { return bindContextFunc().CalcContext; }
                }
            }

            private IValue<TProperty> GetTypedValue(
                Func<TEntity> entityFunc,
                Func<IBindContext<TCalcContext>> bindContextFunc)
            {
                return func(new EntityWrapper(entityFunc, this, bindContextFunc));
            }

            private IValue<TProperty> GetTypedValue(
                object entity,
                Func<IBindContext<TCalcContext>> bindContextFunc)
            {
                return GetTypedValue(() => (TEntity)entity, bindContextFunc);
            }

            public IValue GetValue()
            {
                return GetTypedValue(
                    () => { throw new ApplicationException(); },
                    () => { throw new ApplicationException(); }
                    );
            }

            public bool SetValueToEntity(
                object entity, CalculatedValueCache<TCalcContext> calculatedValueCache)
            {
                return calculatedValueCache.Add(new CalculatedValueCacheItem(this, entity));
            }

            public string PropertyName
            {
                get
                {
                    return propertyExpressionProxy.GetPropertyName();
                }
            }

            private class CalculatedValueCacheItem : ICalculatedValueCacheItem<TCalcContext>
            {
                private readonly FormulaProxy<TEntity, TProperty> parent;
                private readonly object entity;

                public CalculatedValueCacheItem(
                    FormulaProxy<TEntity, TProperty> parent, object entity)
                {
                    this.parent = parent;
                    this.entity = entity;
                }

                public object Entity
                {
                    get { return entity; }
                }

                public string PropertyName
                {
                    get
                    {
                        return parent.PropertyName;
                    }
                }

                private bool valueIsSet;

                public void SetValue(
                    ICalculatedValueCacheFinder<TCalcContext> calculatedValueCacheFinder, 
                    IOneToManyCollectionNewItemProcessor oneToManyCollectionNewItemProcessor, 
                    Func<IBindContext<TCalcContext>> bindContextFunc)
                {
                    if (valueIsSet) return;

                    parent.GetTypedValue(entity, bindContextFunc)
                        .GetValue(
                        new PropertySettor(
                            calculatedValueCacheFinder,
                            oneToManyCollectionNewItemProcessor,
                            bindContextFunc),
                        oneToManyCollectionNewItemProcessor
                        )
                        .ProcessValue(
                        propertyValue => parent.propertyExpressionProxy
                            //TODO: improve performance
                            .GetPropertyInfo().SetValue(entity, propertyValue, null)
                        );

                    valueIsSet = true;
                }

                private class PropertySettor : IPropertySettor
                {
                    private readonly ICalculatedValueCacheFinder<TCalcContext> calculatedValueCacheFinder;
                    private readonly IOneToManyCollectionNewItemProcessor oneToManyCollectionNewItemProcessor;
                    private readonly Func<IBindContext<TCalcContext>> bindContextFunc;

                    public PropertySettor(
                        ICalculatedValueCacheFinder<TCalcContext> calculatedValueCacheFinder,
                        IOneToManyCollectionNewItemProcessor oneToManyCollectionNewItemProcessor,
                        Func<IBindContext<TCalcContext>> bindContextFunc)
                    {
                        this.calculatedValueCacheFinder = calculatedValueCacheFinder;
                        this.oneToManyCollectionNewItemProcessor = oneToManyCollectionNewItemProcessor;
                        this.bindContextFunc = bindContextFunc;
                    }

                    public void SetPropertyValue(IEntityPropertyProxy entityPropertyProxy)
                    {
                        calculatedValueCacheFinder.Find(entityPropertyProxy).ProcessValue(
                            calculatedValueCacheItem =>
                            calculatedValueCacheItem.SetValue(
                                calculatedValueCacheFinder, 
                                oneToManyCollectionNewItemProcessor, 
                                bindContextFunc)
                            );
                    }
                }
            }
        }
    }
}